<template>
  <div>
    <input id="excel-upload-input" type="file" accept=".xlsx, .xls" style="display: none;" @change="handleFile">
    <div id="xh-export-table">
      <FilterTable
        datakey="importTable"
        ref="importTable"
        height="400"
        :on-enter-event="dbclick"
        :data-source="datas"
        :isSelectColumn="false"
        selectionType="single"
        :columns="importTableColumns"
        :isMemoryPagination="true"
        columnsRef="tableData"
        :onColumnsChange="columns => (this.importTableColumns = columns)"
      >
        <template v-slot:controls="controls">
          <el-button v-show="tip.show" @click="openErrorDialog"
                     style="cursor: auto;border: 0">
            <div style="display:flex;">
              <span>{{ tip.title }}</span>
              <i v-if="tip.percentage!=100" class="el-icon-loading" style="margin: 0 5px"/>
              <i v-else class="el-icon-circle-check" style="margin: 0 5px;color: green"/>
              <span v-if="tip.showjd">{{ tip.percentage }}%</span>
            </div>
          </el-button>
          <slot name="downloadTempButton">
            <el-button v-if="isDownloadTemplate" :loading="downloadLoading" type="primary" icon="el-icon-download"
                       @click="downloadTemplate()">下载模板
            </el-button>
          </slot>
          <el-button :loading="loading" type="primary" icon="el-icon-folder-opened" @click="openFile">选择文件</el-button>
          <el-button type="primary" icon="el-icon-delete-solid" @click="clear">清空</el-button>
          <slot slot="controls">

          </slot>
        </template>
      </FilterTable>
    </div>
    <BaseDialog style="min-height: 80vh;" :visible.sync="visibleError" width="1000px" title="错误信息" destroy-on-close
                :containerStyle="{ height: 'calc(100% + 60px)', overflow: 'auto'}">
      <FilterTable
        datakey="errorTable"
        ref="errorTable"
        height="400"
        :data-source="errorDatas"
        exportFileName="导入错误信息.xlsx"
        :isSelectColumn="false"
        selectionType="single"
        :columns="errorTableColumns"
        isMemoryPagination
        columnsRef="errorTable"
        :onColumnsChange="columns => (this.errorTableColumns = columns)"
      >
        <template slot="controls" slot-scope="props">
          <el-button type="primary" @click="exportErrorMsg">导出错误信息</el-button>
        </template>
      </FilterTable>
    </BaseDialog>
    <div class="dialogBottmView">
      <el-button @click="close">取消导入</el-button>
      <el-button :loading="loading2" type="primary" @click="subImport">确定导入</el-button>
    </div>
    <el-dialog append-to-body :visible.sync="dialogVisible2" width="95%" style="margin-top: 20vh">
      <el-table :data="formData" stripe size="mini" border fit highlight-current-row style="flex-grow: 3">
        <el-table-column align="center" label="序" width="60" header-align="center" fixed>
          <template slot-scope="scope">
            <span>{{ scope.$index }}</span>
          </template>
        </el-table-column>
        <edit-table-column v-for="rule in importRules" :key="rule.title" :rule="rule"/>
      </el-table>
    </el-dialog>
  </div>
</template>

<script>
import {dateFormat} from "@/utils";
import _ from 'lodash';
import FileSaver from 'file-saver'
import ExcelJS from 'exceljs/dist/exceljs'

/**
 * 2019.06.12 sunxiaohan
 * 递归组件
 */
const editTableColumn = {
  name: 'edit-table-column',
  props: {rule: {type: Object}},
  template: `
    <el-table-column :prop="rule.prop" :label="rule.title" :width="rule.width" show-overflow-tooltip
                     header-align="center">
    <template slot-scope="scope">
      <el-input v-if="!(rule.children&&rule.children.length)" v-model="scope.row[rule.prop]"/>
    </template>
    <edit-table-column v-if="rule.children&&rule.children.length" v-for="(item, index) in rule.children" :key="index"
                       :rule="item"/>
    </el-table-column> `
}

//后序遍历节点树，获取每个节点树的叶子节点总数
//获取各个节点树的最大层数，返回所有的叶子节点数组
function initExcelTree(tree) {
  const stackArray = [tree];
  const leafNodes = []; //叶子节点集合
  while (stackArray.length) {
    const node = stackArray[stackArray.length - 1];
    if (!node.children) node.children = [];
    const size = node.children.length;
    if (size && !node.$flag) {
      node.$flag = true; //标识已遍历
      node.children.forEach((item, index) => {
        item.$index = index + 1;
        stackArray.push(item);
      });
    } else {
      stackArray.pop();
      const parentIndex = stackArray.length - node.$index;
      if (parentIndex >= 0) {
        const parentNode = stackArray[parentIndex]; //节点的父亲节点
        const $leafSize = node.$leafSize || 1;
        parentNode.$leafSize = (parentNode.$leafSize || 0) + $leafSize; //节点的叶子节点总数
        //父节点最大层数
        parentNode.$maxPlies = Math.max(parentNode.$maxPlies || 0, (node.$maxPlies || 1) + 1);
        if (!node.children.length) {
          node.rule = (node.rule instanceof Array) ? node.rule : [node.rule || {}]
          leafNodes.unshift(node);
        }
      }
    }
  }

  //深度非递归遍历节点，计算出节点在单元格的位置和单元格合并值
  //将节点导入excel,生成excel导入模板
  tree.$row = 0;
  tree.$col = 1;
  tree.$plies = 0;
  const stackArray2 = [tree];
  while (stackArray2.length) {
    const rule = stackArray2.pop() // 出栈
    const $plies = rule.$plies
    const children = rule.children
    rule.$row = rule.$plies;
    rule.$colSpan = rule.$leafSize || 1;
    if (children.length) {
      let $col = rule.$col;
      children.forEach(i => {
        i.$plies = $plies + 1
        i.$col = $col;
        $col += i.$leafSize || 1;
      })
      stackArray2.push(...[...children].reverse());
    } else {
      rule.$rowSpan = tree.$maxPlies - rule.$plies;
    }
  }
  return leafNodes;
}

class ExcelTree {
  constructor(rules) {
    this.children = _.cloneDeep(rules);
    this.leafNodes = initExcelTree(this);
    this.excelColumns = this.leafNodes.map(i => {
      return {
        key: i.prop,
        width: Math.max(10, ~~(i.title.length * 1.65) + 4)
      };
    });
  }

  // 前序遍历方法
  eachNode(callback) {
    const stackArray = [this];
    while (stackArray.length) {
      const node = stackArray.pop(); // 出栈
      const children = node.children;
      if (children.length) {
        stackArray.push(...[...children].reverse());
      }
      if (node !== this) callback(node);
    }
  }
}


export {editTableColumn}
export default {
  name: 'ExcelImport',
  components: {
    editTableColumn
  },
  props: {
    importRules: {
      type: Array, required: true, default() {
        return []
      }
    },
    //下载的导入模板文件名称
    templateFileName: {type: String, default: '导入模板.xlsx'},
    /**
     * 下载模板之前的函数，可向模板中插入数据
     */
    onBeforeDownloadTemp: {
      type: Function,
      default: (callback) => {
        callback()
      }
    },
    //JS验证完成之后的回调函数 （excel数据， 执行函数）
    onComplete: {
      type: Function,
      default: (datas, callback) => {
        callback()
      }
    },
    //是否显示下载模板按钮
    isDownloadTemplate: {default: true, type: Boolean}
  },
  data() {
    return {
      tip: {
        show: false,
        percentage: 0,
        showjd: true
      },
      importTableColumns: this.initImportTableColumns(),
      errorTableColumns: [
        {
          title: '行号',
          width: 100,
          dataIndex: 'rownum',
          sorter: true,
          filter: true,
          filterType: 'input'
        },
        {
          title: '错误信息',
          dataIndex: 'errormsg',
          sorter: true,
          filter: true,
          filterType: 'input',
          render: props => {
            return (<span style="color: red;">{props.row.errormsg}</span>);
          },
        },
        {
          title: '附加信息',
          width: 200,
          dataIndex: 'addmsg',
          sorter: true,
          filter: true,
          filterType: 'input'
        }
      ],
      loading: false,
      downloadLoading: false,
      loading2: false,
      dialogVisible2: false,
      visibleError: false,
      datas: [],
      errorDatas: [],
      formData: [],
    }
  },
  computed: {
    excelTree() {
      return new ExcelTree(this.importRules);
    }
  },
  mounted() {
    this.$nextTick(() => {
    })
  },
  methods: {
    openErrorDialog() {
      if (this.errorDatas.length) {
        this.visibleError = true
        this.errorDatas = [...this.errorDatas]
      }
    },
    // 获取表格column
    getTableColumns(rules) {
      return rules.map(rule => {
        const obj = {
          title: rule.title,
          dataIndex: rule.prop,
          sorter: true,
          filterType: 'input',
          children: rule.children ? this.getTableColumns(rule.children) : []
        };
        const validRules = (rule.rule instanceof Array) ? rule.rule : [rule.rule || {}]
        validRules.forEach(i => {
          if (i.required) {
            obj.editRequired = true;
          }
        })
        return obj;
      })
    },
    initImportTableColumns() {
      const columns = [
        {
          title: '行号',
          dataIndex: 'index',
          width: 70,
          fixed: 'left',
          render: props => {
            return <span>{props.row.index + 1}</span>;
          }
        },
        ...this.getTableColumns(this.importRules)
      ]
      return columns
    },
    dbclick(row, event) {
      this.dialogVisible2 = true
      this.formData = [row]
    },
    clear() {
      this.reset()
      this.setTip({
        show: true,
        title: '清空完成',
        percentage: 100
      })
    },
    reset() {
      this.setTip({
        show: false,
        percentage: 0
      })
      this.datas = []
      this.errorDatas = []
    },
    setTip(tip) {
      this.tip = Object.assign(this.tip, tip)
      this.$forceUpdate()
    },
    close() {
      this.reset()
      this.$emit('close')
      return false
    },
    //导出错误信息
    exportErrorMsg() {
      const workbook = new ExcelJS.Workbook();
      const worksheet = workbook.addWorksheet('错误信息');
      worksheet.columns = [
        {key: 'rownum', header: '行号'},
        {key: 'errormsg', header: '错误信息'},
      ];
      worksheet.addRows(this.errorDatas);
      workbook.xlsx.writeBuffer().then(buffer => {
        FileSaver.saveAs(new Blob([buffer], {type: 'application/octet-stream'}), '错误信息.xlsx')
      });
    },
    /**
     * 模板下载
     * @returns {*}
     */
    downloadTemplate(insertTempData) {
      this.downloadLoading = true;
      const workbook = new ExcelJS.Workbook();
      const worksheet = workbook.addWorksheet('导入模板');
      this.excelTree.eachNode(node => {
        const cell = worksheet.getCell(node.$row, node.$col);
        cell.value = node.title;
        if ((node.$colSpan > 1 || node.$rowSpan > 1) && node.$plies > 0) {
          // 按开始行，开始列，结束行，结束列合并
          worksheet.mergeCells(node.$row, node.$col, node.$row + (node.$rowSpan || 1) - 1, node.$col + (node.$colSpan || 1) - 1);
        }
        //标题单元格背景色
        cell.fill = {
          type: 'pattern',
          pattern: 'solid',
          fgColor: {argb: 'FFB3C2D2'},
        };
        //标题单元格边框
        const border = {style: 'thin', color: {argb: 'FFA8A8A8'}};
        cell.border = {
          top: border,
          left: border,
          bottom: border,
          right: border,
        };
        //标题单元格字体
        cell.font = {bold: true, size: '9'};
        const rules = (node.rule instanceof Array) ? node.rule : [node.rule || {}]
        rules.forEach(i => {
          //必填字段标题红色
          if (i.required) {
            cell.font.color = {argb: 'FFFF0000'}
          }
          //下拉框标题单元格添加提示批注
          if (i.itemList && i.itemList.length) {
            cell.note = `${node.title}值只能为（${Object.values(i.itemList.map(j => j.text))}）。`
          }
        })
        //单元格标题居中
        cell.alignment = {vertical: 'middle', horizontal: 'center'};
      })
      worksheet.columns = this.excelTree.excelColumns;
      //预添加数据
      if (insertTempData) {
        worksheet.addRows(insertTempData);
      }
      new Promise((resolve, reject) => {
        if (this.onBeforeDownloadTemp && this.onBeforeDownloadTemp instanceof Function) {
          this.onBeforeDownloadTemp(datas => {
            if (datas && datas instanceof Array && datas.length) {
              worksheet.addRows(datas);
            }
            resolve()
          })
        } else {
          resolve()
        }
      }).then(r => {
        this.downloadLoading = false;
        workbook.xlsx.writeBuffer().then(buffer => {
          FileSaver.saveAs(new Blob([buffer], {type: 'application/octet-stream'}), this.templateFileName)
        })
      }).catch(e => {
        console.error(e);
        this.downloadLoading = false;
        this.$message({
          message: e.message,
          type: 'error'
        })
      })
    },
    openFile() {
      document.getElementById('excel-upload-input').click()
    },
    handleFile(e) {
      const files = e.target.files
      if (files.length === 1) {
        this.loading = true
        this.setTip({
          show: true,
          title: '导入文件',
          percentage: 1,
          showjd: false
        });
        const file = files[0];
        const reader = new FileReader()
        reader.onload = async e => {
          try {
            const workbook = new ExcelJS.Workbook();
            this.setTip({
              title: '解析数据',
              showjd: false
            });
            await workbook.xlsx.load(reader.result);
            const worksheet = workbook.getWorksheet(1);
            let flag = true; //模板匹配
            this.excelTree.eachNode(node => {
              const cell = worksheet.getCell(node.$row, node.$col);
              if (cell.value != node.title) flag = false; //模板不匹配
            });
            if (!flag) throw new Error('模板不匹配');
            worksheet.columns = this.excelTree.excelColumns;
            const datas = [];
            worksheet.eachRow((row, rowNumber) => {
              if (rowNumber > this.excelTree.$maxPlies - 1) {
                const cellValues = this.excelTree.leafNodes.map((c, index) => {
                  let cellValue = row.getCell(index + 1).value;
                  if (_.isNull(cellValue) || _.isUndefined(cellValue)) {
                    cellValue = '';
                  } else {
                    if (cellValue instanceof Date) {
                      let formatPattern = 'yyyy-MM-dd';
                      c.rule.forEach(ru => {
                        formatPattern = ru.dateFormat || formatPattern
                      })
                      cellValue = dateFormat(formatPattern, cellValue);
                    } else if (cellValue instanceof Number) { //数字类型不做处理
                    } else {
                      cellValue = (cellValue + '').trim();
                    }
                  }
                  return {
                    [c.prop]: cellValue
                  }
                })
                datas.push(Object.assign(...cellValues));
              }
            })
            this.datas = datas;
            this.errorDatas = []
            this.loading = false
            this.setTip({
              title: '解析完成',
              percentage: 100,
              showjd: true
            })
            document.getElementById('excel-upload-input').value = ''// 读取完数据后清空input 要不然无法导入同一个文件
          } catch (e) {
            if (e.message) {
              this.setTip({
                title: e.message
              });
              this.$message({
                message: e.message,
                type: 'error'
              })
            }
            this.setTip({
              percentage: 100,
              showjd: false
            });
            this.loading = false;
            document.getElementById('excel-upload-input').value = ''// 读取完数据后清空input 要不然无法导入同一个文件
          }
        }
        reader.readAsBinaryString(file)
      }
    },
    rulePromise(column, rule = {}, cellValue = '', row) {
      return new Promise((resolve, reject) => {
        if (rule.required && cellValue === '') { // 验证必填
            return reject(rule.message)
        }
        if (rule.pattern && cellValue !== '') { // 验证数据格式
          if (!rule.pattern.test(cellValue)) {
            return reject(rule.message)
          }
        }
        if (rule.itemList && cellValue !== '') { // 验证下拉类型的数据
          let itemList = rule.itemList
          if (Array.isArray(itemList)) {
            itemList = itemList.reduce((obj, i) => {
              obj[i.value] = i.text
              return obj
            }, {})
          }
          const keys = Object.keys(itemList)
          const values = Object.values(itemList)
          if (!values.includes(cellValue)) {
            return reject(rule.message || `${column.title}的值只能为(${values})`)
          } else {
            const obj = {}
            for (let i = 0; i < keys.length; i++) {
              obj[values[i]] = keys[i]
            }
            row[column.prop] = obj[cellValue]
          }
        }
        if (rule.validator) { // 自定义验证方法
          rule.validator(e => {
            if (e instanceof Error) {
              reject(e.message)
            } else {
              resolve()
            }
          }, rule, cellValue, row)
        } else {
          resolve()
        }
      }).catch(e => e)//捕获错误信息，resolve Promise
        .then(e => {
          this.setTip({percentage: (++this.ruleCount / this.ruleSize * 100).toFixed(2)});
          return e;
        });
    },
    cellPromise(column, cellValue, row) {
      let rule = (column.rule instanceof Array) ? column.rule : [column.rule || {}]
      const rulePromiseArr = rule.map(rule => {
        return this.rulePromise(column, rule, cellValue, row);
      });
      return Promise.all(rulePromiseArr).then(e => {
        //过滤正确，分号拼接错误信息
        return e.filter(i => i).join(';');
      });
    },
    rowPromise(row, index) {
      const cellPromiseArr = this.excelTree.leafNodes.map(column => {
        return this.cellPromise(column, row[column.prop], row);
      });
      //返回每行的错误信息
      return Promise.all(cellPromiseArr).then(e => {
        return {
          rownum: index + 1,
          errormsg: e.filter(i => i).join(';')
        }
      });
    },
    validData(datas) {
      const rowPromiseArr = datas.map((row, index) => {
        return this.rowPromise(row, index);
      })
      return Promise.all(rowPromiseArr);
    },
    subImport() { // 确认导入
      if (this.datas.length < 1) {
        return this.$message({
          message: '没有数据可以导入！',
          type: 'warning'
        })
      }
      this.setTip({
        show: true,
        title: '验证数据',
        percentage: 0,
        showjd: true
      })
      this.loading2 = true
      this.errorDatas = []
      const datas = this.datas.map(item => Object.assign({}, item))
      const rowSize = this.excelTree.leafNodes.reduce((total, i) => (total + ((i.rule instanceof Array) ? i.rule.length : 1)), 0)
      const ruleSize = rowSize * this.datas.length // 总单元格数
      this.ruleSize = ruleSize;
      this.ruleCount = 0;
      this.validData(datas).then(e => {
        const errorData = e.filter(i => i.errormsg)
        if (!errorData.length) { // 单元格全部验证通过
          this.setTip({
            show: true,
            title: '正在导入',
            percentage: 1,
            showjd: false
          })
          this.onComplete(datas, errorDatas => {
            this.loading2 = false
            this.setTip({
              show: true,
              title: `验证完成${errorDatas ? '（数据存在错误）' : ''}`,
              percentage: 100,
              showjd: true
            })
            if (errorDatas) {
              this.visibleError = true
              this.errorDatas = errorDatas
            }
          })
        } else { // 有错误
          this.loading2 = false
          this.errorDatas = errorData;
          this.setTip({
            show: true,
            title: '验证完成（数据存在错误）',
            percentage: 100
          })
          this.visibleError = true
        }
      })
    },
    /**
     * 组件暴露方法，下载模板
     * insertTempData 可以向模板插入数据
     */
    DOWNLOAD_TEMPLATE(insertTempData) {
      this.downloadTemplate(insertTempData)
    },
    /**
     * 组件暴露方法，提交导入
     */
    SUBMIT_IMPORT() {
      this.subImport()
    }
  }
}
</script>
